/*
 * Copyright (c) 2017 Jacob Rachiele
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without restriction
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to
 * do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * Contributors:
 *
 * Jacob Rachiele
 */

package com.github.signaflo.timeseries;

import org.hamcrest.MatcherAssert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertArrayEquals;

public class TimeSeriesSpec {

    private TimeSeries timeSeries = TestData.ausbeer;

    @Rule
    public ExpectedException exception = ExpectedException.none();

    @Test
    public void whenAtIndexLessThanZeroThenIndexOutOfBoundsException() {
        exception.expect(IndexOutOfBoundsException.class);
        timeSeries.at(-3);
    }

    @Test
    public void whenAtIndexGreaterThanSeriesLengthThenIndexOutOfBoundsException() {
        exception.expect(IndexOutOfBoundsException.class);
        timeSeries.at(timeSeries.size() + 1);
    }

    @Test
    public void whenAtIndexEqualToSeriesLengthThenIndexOutOfBoundsException() {
        exception.expect(IndexOutOfBoundsException.class);
        timeSeries.at(timeSeries.size());
    }

    @Test
    public void whenDateTimeIndexNonExistentThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        Time time = Time.fromYear(1);
        timeSeries.at(time);
    }

    @Test
    public void whenAggregateWithSmallerPeriodThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        timeSeries.aggregate(TimePeriod.oneMonth());
    }

    @Test
    public void whenBoxCoxBackTransformLambdaTooBigThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        timeSeries.backTransform(2.5);
    }

    @Test
    public void whenBoxCoxTransformationLambdaTooBigThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        timeSeries.transform(2.5);
    }

    @Test
    public void whenBoxCoxBackTransformLambdaTooSmallThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        timeSeries.backTransform(-1.5);
    }

    @Test
    public void whenBoxCoxTransformationLambdaTooSmallThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        timeSeries.transform(-1.5);
    }

    @Test
    public void whenBoxCoxTransformLogThenDataTransformedCorrectly() {
        double[] data = new double[]{3.0, 7.0, Math.E};
        double[] expected = new double[]{Math.log(3.0), Math.log(7.0), 1.0};
        timeSeries = TimeSeries.from(data);
        assertArrayEquals(expected, timeSeries.transform(0).asArray(), 1E-4);
    }

    @Test
    public void whenBoxCoxInvTransformLogThenDataTransformedCorrectly() {
        double[] data = new double[]{Math.log(3.0), Math.log(7.0), 1.0};
        double[] expected = new double[]{3.0, 7.0, Math.E};
        timeSeries = TimeSeries.from(data);
        assertArrayEquals(expected, timeSeries.backTransform(0).asArray(), 1E-4);
    }


    @Test
    public void whenDifferenceWithLagLessThanOneThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        TimeSeries.difference(TestData.ausbeerArray, 0, 1);
    }

    @Test
    public void whenDifferenceWithLagGreaterThanSeriesLengthThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        TimeSeries.difference(TestData.ausbeerArray, TestData.ausbeerArray.length + 1, 1);
    }

    @Test
    public void whenDifferenceWithNegativeTimesThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        TimeSeries.difference(TestData.ausbeerArray, 1, -1);
    }

    @Test
    public void whenDifferencedZeroTimesThenOriginalSeries() {
        assertThat(TimeSeries.difference(TestData.ausbeerArray, 1, 0), is(TestData.ausbeerArray));
    }

    @Test
    public void whenDifferencedNoArgThenDifferencedOnce() {
        assertThat(timeSeries.difference(), is(timeSeries.difference(1)));
    }

    @Test
    public void whenDifferencedMoreThanOnceThenCorrectData() {
        TimeSeries diffedTwice = timeSeries.difference().difference();
        assertThat(timeSeries.difference(1, 2), is(diffedTwice));
    }

    @Test
    public void whenStartTimeThenFirstObservationTime() {
        timeSeries = Ts.newQuarterlySeries(1956, 1, TestData.ausbeerArray);
        Time expected = Time.fromYear(1956);
        assertThat(timeSeries.startTime(), is(expected));
    }

    @Test
    public void whenAtDateTimeAccessedThenCorrectValueReturned() {
        timeSeries = TestData.debitcards;
        Time year2000 = Time.fromYear(2000);
        assertThat(timeSeries.at(year2000), is(timeSeries.at(0)));
    }

    @Test
    public void whenTimeSeriesMeanTakenThenResultCorrect() {
        double[] data = new double[]{3.0, 7.0, 5.0};
        TimeSeries series = TimeSeries.from(Time.now(), data);
        assertThat(series.mean(), is(equalTo(5.0)));
    }

    @Test
    public void whenAutoCovarianceNegativeLagThenIllegalArgument() {
        exception.expect(IllegalArgumentException.class);
        timeSeries.autoCovarianceAtLag(-1);
    }

    @Test
    public void whenAutoCovarianceComputedTheResultIsCorrect() {
        TimeSeries series = TimeSeries.from(10.0, 5.0, 4.5, 7.7, 3.4, 6.9);
        double[] acvf = new double[]{4.889, -1.837, -0.407, 1.310, -1.917, 0.406};
        for (int i = 0; i < acvf.length; i++) {
            assertThat(series.autoCovarianceAtLag(i), is(closeTo(acvf[i], 1E-2)));
        }
    }

    @Test
    public void whenAutoCorrelationComputedTheResultIsCorrect() {
        TimeSeries series = TimeSeries.from(10.0, 5.0, 4.5, 7.7, 3.4, 6.9);
        double[] acf = new double[]{1.000, -0.376, -0.083, 0.268, -0.392, 0.083};
        for (int i = 0; i < acf.length; i++) {
            assertThat(series.autoCorrelationAtLag(i), is(closeTo(acf[i], 1E-2)));
        }
    }

    @Test
    public void whenAutoCovarianceComputedUpToLagKThenResultingArrayCorrect() {
        TimeSeries series = TimeSeries.from(10.0, 5.0, 4.5, 7.7, 3.4, 6.9);
        double[] expected = new double[]{4.889, -1.837, -0.407, 1.310, -1.917, 0.406};
        double[] result = series.autoCovarianceUpToLag(9);
        assertArrayEquals(expected, result, 1E-2);
    }

    @Test
    public void whenAutoCorrelationComputedUpToLagKThenResultingArrayCorrect() {
        TimeSeries series = TimeSeries.from(10.0, 5.0, 4.5, 7.7, 3.4, 6.9);
        double[] expected = new double[]{1.000, -0.376, -0.083, 0.268, -0.392, 0.083};
        double[] result = series.autoCorrelationUpToLag(5);
        assertArrayEquals(expected, result, 1E-2);
    }

    @Test
    public void whenFivePeriodMovingAverageComputedResultCorrect() {
        TimeSeries series = TestData.elecSales;
        double[] expected = new double[]{2381.53, 2424.556, 2463.758, 2552.598, 2627.7, 2750.622, 2858.348, 3014.704,
                3077.3, 3144.52, 3188.7, 3202.32, 3216.94, 3307.296, 3398.754, 3485.434};
        double[] result = series.movingAverage(5).asArray();
        assertArrayEquals(expected, result, 1E-2);
    }

    @Test
    public void whenFourPeriodMovingAverageComputedResultCorrect() {
        TimeSeries series = TestData.elecSales;
        double[] expected = new double[]{2380.39, 2388.3275, 2435.7675, 2500.0675, 2573.5, 2688.1025, 2795.91,
                2929.005, 3077.7, 3135.5, 3180.475, 3208.85, 3163.525, 3252.25, 3338.97, 3443.0425, 3562.7425};
        double[] result = series.movingAverage(4).asArray();
        assertArrayEquals(expected, result, 1E-2);
    }

    @Test
    public void whenFourPeriodCenteredMovingAverageComputedResultCorrect() {
        TimeSeries series = TestData.elecSales;
        double[] expected = new double[]{2384.35875, 2412.0475, 2467.9175, 2536.78375, 2630.80125, 2742.00625,
                2862.4575, 3003.3525, 3106.6, 3157.9875, 3194.6625, 3186.1875, 3207.8875, 3295.61, 3391.00625,
                3502.8925};
        double[] result = series.centeredMovingAverage(4).asArray();
        assertArrayEquals(expected, result, 1E-2);
    }

    @Test
    public void whenFivePeriodCenteredMovingAverageComputedResultCorrect() {
        TimeSeries series = TestData.elecSales;
        double[] expected = new double[]{2381.53, 2424.556, 2463.758, 2552.598, 2627.7, 2750.622, 2858.348, 3014.704,
                3077.3, 3144.52, 3188.7, 3202.32, 3216.94, 3307.296, 3398.754, 3485.434};
        double[] result = series.centeredMovingAverage(5).asArray();
        assertArrayEquals(expected, result, 1E-2);
    }

    @Test
    public void whenTimeSeriesAggregatedDatesCorrect() {
        TimeSeries aggregated = timeSeries.aggregate(TimePeriod.oneDecade());
        Time expectedStart = Time.fromYear(1956);
        Time expectedEnd = Time.fromYear(1996);
        assertThat(aggregated.observationTimes().get(0), is(equalTo(expectedStart)));
        assertThat(aggregated.observationTimes().get(aggregated.size() - 1), is(equalTo(expectedEnd)));
    }

    @Test
    public void whenWeeklySeriesCreatedResultCorrect() {
        TimeSeries series = TestData.sydneyAir;
        TimeSeries seriesOne = series.aggregateToYears();
        TimeSeries seriesTwo = series.aggregate(TimePeriod.oneYear());
        MatcherAssert.assertThat(seriesOne, is(equalTo(seriesTwo)));
    }

    @Test
    public void whenFromThenCorrectSliceOfDataReturned() {
        double[] ts = TestData.ausbeerArray;
        TimeSeries series = TimeSeries.from(TimePeriod.oneQuarter(), "1956-01-01T00:00:00", ts);
        Time start = Time.fromYearMonth(1956, 4);
        Time end = Time.fromYear(1957);
        TimeSeries expected = series.timeSlice(2, 5);
        assertThat(series.slice(start, end), is(expected));
        assertThat(series.slice(1, 4), is(expected));
    }

    @Test
    public void testHashCodeAndEquals() {
        double[] ts = TestData.ausbeerArray;
        TimeSeries series1 = Ts.newQuarterlySeries(1956, 1, ts);
        TimeSeries series2 = Ts.newQuarterlySeries(1957, 2, ts);
        TimeSeries series3 = TestData.elecSales;
        TimeSeries series4 = Ts.newQuarterlySeries(1956, 1, ts);
        TimeSeries nullSeries = null;
        String aNonTimeSeries = "";
        assertThat(series1, is(series1));
        assertThat(series1, is(series4));
        assertThat(series1.hashCode(), is(series4.hashCode()));
        MatcherAssert.assertThat(series1, is(not(nullSeries)));
        MatcherAssert.assertThat(series1, is(not(aNonTimeSeries)));
        MatcherAssert.assertThat(series1, is(not(series2)));
        MatcherAssert.assertThat(series2, is(not(series3)));
    }
}
